package org.beanutopia;

import com.google.common.base.Function;
import com.google.common.base.Nullable;
import static org.beanutopia.function.CompositeFunction.getGenericParameterClasses;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;

/**
 * @author ymeymann
 * @since Nov 15, 2007 7:53:43 PM
 */
public abstract class AbstractProperty<B,V> implements Serializable, Function<B,V> {
  protected final Class<B> beanType;
  protected final Class<V> valueType;
  protected final Class<?> namespace;
  protected String name;
  protected boolean initialized = false;
  protected boolean initializedName = false;
  final protected V defaultValue;

  protected AbstractProperty() {
    this(null);
  }

  protected AbstractProperty(V defaultValue) {
    //set the default value for the property
    this.defaultValue = defaultValue;
    this.name = getClass().getName().substring(getClass().getName().lastIndexOf('.') + 1); //name == null ? ... : name;

    //find containing class
    namespace = getClass().getEnclosingClass(); // == null ? getClass().getDeclaringClass() : getClass().getEnclosingClass();

    //TODO make sure that the field is static and final?
    //TODO maybe check that name of the property matches the definition
    //TODO check that generic definition on the left matches the one on the right?
    //TODO maybe limit to namespace = beanType, disallow "dynamic property addition"?

    Type superclass = getClass().getGenericSuperclass();
    Class[] c = getGenericParameterClasses(superclass);
    beanType = castBean(c.length > 0 ? c[0] : Object.class);
    valueType = castValue(c.length > 1 ? c[1] : Object.class);
    //TODO incorporate test to disallow parametric types, as per foloow-up post by Neal Gafter

    //register property
    PropertyUtils.addProperty(this, beanType());
  }

  @SuppressWarnings({"unchecked"})
  private Class<B> castBean(Class<?> c) { return (Class<B>)c; }

  @SuppressWarnings({"unchecked"})
  private Class<V> castValue(Class<?> c) { return (Class<V>)c; }

  @SuppressWarnings({"unchecked"})
  public final Class<? extends AbstractProperty<B,V>> id() {
    return (Class<? extends AbstractProperty<B,V>>) getClass();
  }

  public final boolean isArray() {
    return valueType().isArray();
  }

  public final Class<B> beanType() { return beanType; }

  public final Class<V> valueType() { return valueType; }

  public final String name() { return name; }

  public final Class<?> namespace() { return namespace; }

  public final Annotation[] annotations() {
    initialize();
    if (namespace() == null) return null;
    try {
      return namespace().getField(name()).getAnnotations();
    } catch (NoSuchFieldException e) {
      return null;
    }
  }

  protected void writeObject(java.io.ObjectOutputStream out)
     throws IOException {
    initialize();
    out.defaultWriteObject();
  }

  protected Object readResolve() throws ObjectStreamException {
    if (namespace() == null) return this;
    try {
      return namespace().getField(name()).get(null);
    } catch (Exception e) {
      return this;
    }
  }

  /**
   * This method can only be called after the property object has been assigned to a static field
   */
  public final void initialize() {
    if (initialized) return;
    initName();
    init();
    initialized = true;
  }

  protected abstract void init() ;

  protected final void initName() {
    if (initializedName || namespace() == null) return;
    for (Field f : namespace().getDeclaredFields()) { //it's my "declaring" class, I should have access to all its fields
        if (AbstractProperty.class.isAssignableFrom(f.getType()) && ((f.getModifiers() & Modifier.STATIC) > 0)) {
          Object ref;
          try {
            ref = f.get(null);
          } catch (Exception e) {
            ref = null;
          }
          if (ref == this) {
            name = f.getName().intern();
            initializedName = true;
            break;
          }
        }
    }
  }

  public V defaultValue() { return defaultValue; }

  public final V of(B object) {
    initialize();
    V res = get(object);
    return res == null ? defaultValue() : res;
  }
  
  abstract protected V get(B object);

  @Override final public V apply(@Nullable B b) {
    return of(b);
  }

}
